﻿' BasecampVB - http://www.tidbitsandtangents.com
' Copyright (c) 2009
' by Rusty S. Swayne
'
' Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 
' documentation files (the "Software"), to deal in the Software without restriction, including without limitation 
' the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and 
' to permit persons to whom the Software is furnished to do so, subject to the following conditions:
'
' The above copyright notice and this permission notice shall be included in all copies or substantial portions 
' of the Software.
'
' THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
' INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
' DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
' SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
' SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
' WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
' THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.


Imports System.Web
Imports System.Xml
Imports System.Text
Imports System.IO

Namespace DAL

    Public Class BasecampRestAPIDataProvider
        Inherits ApiAccess

        ''' <summary>
        ''' Constructor
        ''' </summary>
        Public Sub New()
            With TnT.BasecampVB.API.Settings
                Me.basecamp_url = .BasecampUrl
                Me.api_user_name = .Username
                Me.api_password = .Password
            End With
        End Sub



#Region "Account"

        ''' <summary>
        ''' Returns AccountDetails from Basecamp API
        ''' </summary>
        Public Function GetAccount() As AccountDetails
            Return GetAccountDetailsFromXmlNode(GetXmlDocumentResponse("account.xml").DocumentElement)
        End Function

        ''' <summary>
        ''' Helper Function to create AccountDetails from XmlNode
        ''' </summary>
        Private Shared Function GetAccountDetailsFromXmlNode(ByVal node As XmlNode) As AccountDetails
            If node IsNot Nothing Then
                Return New AccountDetails( _
                    GetChildNodeInnerTextAsInt(node, "id"), _
                    GetChildNodeInnerTextAsString(node, "name"), _
                    GetChildNodeInnerTextAsInt(node, "account-holder-id"), _
                    GetChildNodeInnerTextAsBoolean(node, "ssl-enabled"), _
                    GetChildNodeInnerTextAsBoolean(node, "email-notification-enabled"), _
                    GetChildNodeInnerTextAsBoolean(node, "time-tracking-enabled"), _
                    GetChildNodeInnerTextAsDateTime(node, "updated-at"))
            Else
                Return Nothing
            End If
        End Function

#End Region


#Region "Projects"

        ''' <summary>
        ''' Returns a List of all projects as List(Of ProjectDetails) from Basecamp API
        ''' </summary>
        Public Function GetProjects() As List(Of ProjectDetails)            
            Return GetProjectDetailsCollectionFromXmlDocument(GetXmlDocumentResponse("projects.xml"))
        End Function

        ''' <summary>
        ''' Returns ProjectDetails from Basecamp API
        ''' </summary>
        Public Function GetProject(ByVal project_id As Integer) As ProjectDetails
            Return GetProjectDetailsFromXmlNode(GetXmlDocumentResponse(String.Format("projects/{0}.xml", project_id.ToString)).DocumentElement)
        End Function

        ''' <summary>
        ''' Helper Function to create List(Of ProjectDetails) from a XmlDocument nodes
        ''' </summary>
        Private Shared Function GetProjectDetailsCollectionFromXmlDocument(ByVal doc As XmlDocument) As List(Of ProjectDetails)
            Dim details As New List(Of ProjectDetails)
            Dim nodelist As XmlNodeList = doc.SelectNodes("//project")
            Dim node As XmlNode = Nothing
            For Each node In nodelist
                details.Add(GetProjectDetailsFromXmlNode(node))
            Next
            Return details
        End Function

        ''' <summary>
        ''' Helper Function to create ProjectDetails from XmlNode
        ''' </summary>
        Private Shared Function GetProjectDetailsFromXmlNode(ByVal node As XmlNode) As ProjectDetails

            Return New ProjectDetails( _
                GetChildNodeInnerTextAsInt(node, "id"), _
                GetChildNodeInnerTextAsInt(node, "company/id"), _
                GetChildNodeInnerTextAsString(node, "announcement"), _
                GetChildNodeInnerTextAsString(node, "name"), _
                GetChildNodeInnerTextAsString(node, "start-page"), _
                GetChildNodeInnerTextAsBoolean(node, "show-announcement"), _
                GetChildNodeInnerTextAsBoolean(node, "show-writeboards"), _
                GetChildNodeInnerTextAsString(node, "status"), _
                GetChildNodeInnerTextAsDateTime(node, "created-on"))

        End Function

#End Region

#Region "People"

        ''' <summary>
        ''' Returns PersonDetails for API user from Basecamp API
        ''' </summary>
        Public Function GetMe() As PersonDetails
            Return GetPersonDetailsFromXmlNode(GetXmlDocumentResponse("me.xml").DocumentElement)
        End Function

        ''' <summary>
        ''' Returns PersonDetails from Basecamp API
        ''' </summary>
        Public Function GetPerson(ByVal person_id As Integer) As PersonDetails
            Return GetPersonDetailsFromXmlNode(GetXmlDocumentResponse(String.Format("/people/{0}.xml", person_id.ToString)))
        End Function

        ''' <summary>
        ''' Returns a list of all people as List(Of PersonDetails) from Basecamp API
        ''' </summary>
        Public Function GetPeople() As List(Of PersonDetails)
            Return GetPersonDetailsCollectionFromXmlDocument(GetXmlDocumentResponse("people.xml"))
        End Function

        ''' <summary>
        ''' Returns a list of all people associated with a project as List(Of PersonDetails) from Basecamp API
        ''' </summary>
        Public Function GetPeopleByProject(ByVal project_id As Integer) As List(Of PersonDetails)
            Return GetPersonDetailsCollectionFromXmlDocument(GetXmlDocumentResponse(String.Format("projects/{0}/people.xml", project_id.ToString)))
        End Function

        ''' <summary>
        ''' Returns a list of all people associated with a company as List(Of PersonDetails) from Basecamp API
        ''' </summary>
        Public Function GetPeopleByCompany(ByVal company_id As Integer) As List(Of PersonDetails)
            Return GetPersonDetailsCollectionFromXmlDocument(GetXmlDocumentResponse(String.Format("companies/{0}/people.xml", company_id.ToString)))
        End Function

        ''' <summary>
        ''' Helper Function to create a List(Of Person) from XmlDocument nodes
        ''' </summary>
        Private Shared Function GetPersonDetailsCollectionFromXmlDocument(ByVal doc As XmlDocument) As List(Of PersonDetails)
            Dim people As New List(Of PersonDetails)
            Dim nodelist As XmlNodeList = doc.SelectNodes("//person")
            Dim node As XmlNode = Nothing
            For Each node In nodelist
                people.Add(GetPersonDetailsFromXmlNode(node))
            Next
            Return people
        End Function

        ''' <summary>
        ''' Helper Function to create PersonDetails from XmlNode
        ''' </summary>
        Private Shared Function GetPersonDetailsFromXmlNode(ByVal node As XmlNode) As PersonDetails

            Return New PersonDetails( _
                GetChildNodeInnerTextAsInt(node, "id"), _
                GetChildNodeInnerTextAsInt(node, "client-id"), _
                GetChildNodeInnerTextAsString(node, "user-name"), _
                GetChildNodeInnerTextAsString(node, "first-name"), _
                GetChildNodeInnerTextAsString(node, "last-name"), _
                GetChildNodeInnerTextAsString(node, "title"), _
                GetChildNodeInnerTextAsString(node, "email-address"), _
                GetChildNodeInnerTextAsString(node, "phone-number-fax"), _
                GetChildNodeInnerTextAsString(node, "phone-number-home"), _
                GetChildNodeInnerTextAsString(node, "phone-number-mobile"), _
                GetChildNodeInnerTextAsString(node, "phone-number-office"), _
                GetChildNodeInnerTextAsString(node, "phone-number-office-ext"), _
                GetChildNodeInnerTextAsString(node, "identity-url"), _
                GetChildNodeInnerTextAsString(node, "im-handle"), _
                GetChildNodeInnerTextAsString(node, "im-service"), _
                GetChildNodeInnerTextAsBoolean(node, "has-access-to-new-projects"), _
                GetChildNodeInnerTextAsBoolean(node, "administrator"), _
                GetChildNodeInnerTextAsBoolean(node, "deleted"), _
                New Guid(GetChildNodeInnerTextAsString(node, "uuid")))

        End Function

#End Region

#Region "Company"


        ''' <summary>
        ''' Returns a List(Of CompanyDetails) of all companies from Basecamp API
        ''' </summary>
        Public Function GetCompanies() As List(Of CompanyDetails)
            Return GetCompanyDetailsCollectionFromXmlDocument(GetXmlDocumentResponse("companies.xml"))
        End Function

        ''' <summary>
        ''' Returns a List(Of CompanyDetails) of companies associated with a project from Basecamp API
        ''' </summary>
        Public Function GetCompaniesByProject(ByVal project_id As Integer) As List(Of CompanyDetails)
            Return GetCompanyDetailsCollectionFromXmlDocument(GetXmlDocumentResponse(String.Format("projects/{0}/companies.xml", project_id)))
        End Function

        ''' <summary>
        ''' Returns CompanyDetails from Basecamp API
        ''' </summary>
        Public Function GetCompany(ByVal company_id As Integer) As CompanyDetails
            Return GetCompanyDetailsFromXmlNode(GetXmlDocumentResponse(String.Format("companies/{0}.xml", company_id.ToString)))
        End Function

        ''' <summary>
        ''' Helper Function to create a List(Of Company) from XmlDocument nodes
        ''' </summary>
        Private Shared Function GetCompanyDetailsCollectionFromXmlDocument(ByVal doc As XmlDocument) As List(Of CompanyDetails)
            Dim details As New List(Of CompanyDetails)
            Dim nodelist As XmlNodeList = doc.SelectNodes("//company")
            Dim node As XmlNode = Nothing
            For Each node In nodelist
                details.Add(GetCompanyDetailsFromXmlNode(node))
            Next
            Return details
        End Function

        ''' <summary>
        ''' Helper Function to create CompanyDetails from XmlNode
        ''' </summary>
        Private Shared Function GetCompanyDetailsFromXmlNode(ByVal node As XmlNode) As CompanyDetails
            Return New CompanyDetails( _
                GetChildNodeInnerTextAsInt(node, "id"), _
                GetChildNodeInnerTextAsString(node, "name"), _
                GetChildNodeInnerTextAsString(node, "address-one"), _
                GetChildNodeInnerTextAsString(node, "address-two"), _
                GetChildNodeInnerTextAsString(node, "city"), _
                GetChildNodeInnerTextAsString(node, "state"), _
                GetChildNodeInnerTextAsString(node, "zip"), _
                GetChildNodeInnerTextAsString(node, "country"), _
                GetChildNodeInnerTextAsString(node, "phone-number-fax"), _
                GetChildNodeInnerTextAsString(node, "phone-number-office"), _
                GetChildNodeInnerTextAsString(node, "time-zone-id"), _
                GetChildNodeInnerTextAsString(node, "web-address"))
        End Function

#End Region

#Region "Category"

        ''' <summary>
        ''' Returns CategoryDetails from Basecamp API
        ''' </summary>
        Public Function GetCategory(ByVal category_id As Integer) As CategoryDetails
            Return GetCategoryFromXmlNode(GetXmlDocumentResponse(String.Format("categories/{0}.xml", category_id)))
        End Function

        ''' <summary>
        ''' Returns List(Of CategoryDetails) for a given project with an optional filter
        ''' </summary>
        Public Function GetCategories(ByVal project_id As Integer, Optional ByVal filter As String = "") As List(Of CategoryDetails)
            Dim requestString As String = String.Format("projects/{0}/categories.xml", project_id.ToString)
            If Not String.IsNullOrEmpty(filter) Then requestString = String.Format("{0}?type={1}", requestString, filter)
            Return GetCategoryDetailsCollectionFromXmlDocument(GetXmlDocumentResponse(requestString))
        End Function

        ''' <summary>
        ''' Creates a Basecamp category in a given project in Basecamp
        ''' </summary>
        Public Function CreateCategory(ByVal project_id As Integer, ByVal type As String, ByVal name As String) As Integer
            Dim requestXml As String = String.Format("<category><type>{0}</type><name>{1}</name></category>", type, HttpUtility.HtmlEncode(name))
            Dim url As String = GetResponse(String.Format("projects/{0}/categories.xml", project_id.ToString), "POST", requestXml)
            Return CType(url.Replace(basecamp_url & "categories/", String.Empty).Replace(".xml", String.Empty), Integer)
        End Function

        ''' <summary>
        ''' Updates a Basecamp category in a given project in Basecamp
        ''' </summary>
        Public Function UpdateCategory(ByVal category_id As Integer, ByVal name As String) As Boolean
            Dim requestXml As String = String.Format("<category><name>{0}</name></category>", HttpUtility.HtmlEncode(name))
            Dim resp As String = GetResponse(String.Format("categories/{0}.xml", category_id.ToString), "PUT", requestXml)
            Return String.IsNullOrEmpty(resp.Trim)
        End Function

        ''' <summary>
        ''' Deletes a Basecamp category in a given project in Basecamp
        ''' </summary>
        Public Function DeleteCategory(ByVal category_id As Integer) As Boolean
            Return String.IsNullOrEmpty(GetResponse(String.Format("categories/{0}.xml", category_id.ToString), "DELETE").Trim)
        End Function


        ''' <summary>
        ''' Helper Function to create a List(Of Company) from XmlDocument nodes
        ''' </summary>
        Private Shared Function GetCategoryDetailsCollectionFromXmlDocument(ByVal doc As XmlDocument) As List(Of CategoryDetails)
            Dim categories As New List(Of CategoryDetails)
            Dim nodelist As XmlNodeList = doc.SelectNodes("//category")
            Dim node As XmlNode = Nothing
            For Each node In nodelist
                categories.Add(GetCategoryFromXmlNode(node))
            Next
            Return categories
        End Function

        ''' <summary>
        ''' Helper Function to create CategoryDetails from XmlNode
        ''' </summary>
        Private Shared Function GetCategoryFromXmlNode(ByVal node As XmlNode) As CategoryDetails
            Return New CategoryDetails( _
                GetChildNodeInnerTextAsInt(node, "id"), _
                GetChildNodeInnerTextAsInt(node, "project-id"), _
                GetChildNodeInnerTextAsString(node, "name"), _
                GetChildNodeInnerTextAsInt(node, "elements-count"), _
                GetChildNodeInnerTextAsString(node, "type"))
        End Function

#End Region

#Region "Messages"

        ''' <summary>
        ''' Returns the 25 most recent messages in the given project
        ''' </summary>
        Public Function GetPosts(ByVal project_id As Integer) As List(Of PostDetails)
            Return GetPostDetailsCollectionFromXmlDocument(GetXmlDocumentResponse(String.Format("projects/{0}/posts.xml", project_id.ToString)))
        End Function

        ''' <summary>
        ''' Returns PostDetails from Basecamp API (Basecamp Message)
        ''' </summary>
        Public Function GetPost(ByVal post_id As Integer) As PostDetails
            Return GetPostDetailsFromXmlNode(GetXmlDocumentResponse(String.Format("posts/{0}.xml", post_id.ToString)))
        End Function

        ''' <summary>
        ''' Returns PostDetails by category from Basecamp API (Basecamp Message)
        ''' </summary>
        Public Function GetPostsByCategory(ByVal project_id As Integer, ByVal category_id As Integer) As List(Of PostDetails)
            Return GetPostDetailsCollectionFromXmlDocument(GetXmlDocumentResponse(String.Format("projects/{0}/cat/{1}/posts.xml", project_id.ToString, category_id.ToString)))
        End Function

        ''' <summary>
        ''' Returns PostDetails of archived posts from Basecamp API (Basecamp Message)
        ''' </summary>
        Public Function GetArchivedPosts(ByVal project_id As Integer) As List(Of PostDetails)
            Return GetPostDetailsCollectionFromXmlDocument(GetXmlDocumentResponse(String.Format("projects/{0}/posts/archive.xml", project_id.ToString)))
        End Function

        ''' <summary>
        ''' Returns PostDetails of archived posts by category from Basecamp API (Basecamp Message)
        ''' </summary>
        Public Function GetArchivedPostsByCategory(ByVal project_id As Integer, ByVal category_id As Integer) As List(Of PostDetails)
            Return GetPostDetailsCollectionFromXmlDocument(GetXmlDocumentResponse(String.Format("projects/{0}/cat/{1}/posts/archive.xml", project_id.ToString, category_id.ToString)))
        End Function

        ''' <summary>
        ''' Creates a Message in Basecamp via Basecamp API (Basecamp Message)
        ''' </summary>
        Public Function CreatePost(ByVal project_id As Integer, ByVal category_id As Integer, ByVal milestone_id As Integer, _
            ByVal title As String, ByVal body As String, ByVal extended_body As String, ByVal is_private As Boolean, ByVal notify As List(Of Integer)) As Integer
            Dim url As String = GetResponse(String.Format("projects/{0}/posts.xml", project_id.ToString), "POST", GetPostRequestXml(category_id, milestone_id, title, body, extended_body, is_private, notify))
            Return CType(url.Replace("/posts/", String.Empty).Replace(".xml", String.Empty), Integer)
        End Function

        ''' <summary>
        ''' Update a Basecamp Message via Basecamp API (Basecamp Message)
        ''' </summary>
        Public Function UpdatePost(ByVal post_id As Integer, ByVal category_id As Integer, ByVal milestone_id As Integer, _
            ByVal title As String, ByVal body As String, ByVal extended_body As String, ByVal is_private As Boolean, ByVal notify As List(Of Integer)) As Integer
            Dim resp As String = GetResponse(String.Format("/posts/{0}.xml", post_id.ToString), "PUT", GetPostRequestXml(category_id, milestone_id, title, body, extended_body, is_private, notify))
            Return String.IsNullOrEmpty(resp.Trim)
        End Function

        ''' <summary>
        ''' Deletes a Message in Basecamp via Basecamp API (Basecamp Message)
        ''' </summary>
        Public Function DeletePost(ByVal post_id As Integer) As Boolean
            Return String.IsNullOrEmpty(GetResponse(String.Format("posts/{0}.xml", post_id.ToString), "DELETE").Trim)
        End Function

        ''' <summary>
        ''' Helper function to create Request Xml for Post (message) requests
        ''' </summary>
        Private Function GetPostRequestXml(ByVal category_id As Integer, ByVal milestone_id As Integer, _
            ByVal title As String, ByVal body As String, ByVal extended_body As String, ByVal is_private As Boolean, ByVal notify As List(Of Integer)) As String
            Dim sb As New StringBuilder
            sb.Append("<request><post>")
            If category_id = 0 Then
                sb.Append("<category-id />")
            Else
                sb.Append(String.Format("<category-id>{0}</category-id>", category_id.ToString))
            End If
            If category_id = 0 Then
                sb.Append("<milestone-id />")
            Else
                sb.Append(String.Format("<milestone-id>{0}</milestone-id>", milestone_id.ToString))
            End If
            sb.Append(String.Format("<body>{0}</body>", HttpUtility.HtmlEncode(body)))
            sb.Append(String.Format("<extended-body>{0}</extended-body>", HttpUtility.HtmlEncode(extended_body)))
            sb.Append(String.Format("<private>{0}</private>", is_private.ToString))
            sb.Append(String.Format("<title>{0}</title>", HttpUtility.HtmlEncode(title)))
            sb.Append("</post>")

            If notify IsNot Nothing Then

                Dim person_id As Integer = 0
                For Each person_id In notify
                    sb.Append(String.Format("<notify>{0}</notify>", person_id.ToString))
                Next

            End If
            sb.Append("</request>")
            Return sb.ToString
        End Function

        ''' <summary>
        ''' Helper Function to create a List(Of PostDetails) from XmlDocument nodes
        ''' </summary>
        Private Shared Function GetPostDetailsCollectionFromXmlDocument(ByVal doc As XmlDocument) As List(Of PostDetails)
            Dim posts As New List(Of PostDetails)
            Dim nodelist As XmlNodeList = doc.SelectNodes("//post")
            Dim node As XmlNode = Nothing
            For Each node In nodelist
                posts.Add(GetPostDetailsFromXmlNode(node))
            Next
            Return posts
        End Function

        ''' <summary>
        ''' Helper Function to create PostDetails from XmlNode
        ''' </summary>
        Private Shared Function GetPostDetailsFromXmlNode(ByVal node As XmlNode) As PostDetails
            Return New PostDetails( _
                GetChildNodeInnerTextAsInt(node, "id"), _
                GetChildNodeInnerTextAsInt(node, "project-id"), _
                GetChildNodeInnerTextAsString(node, "title"), _
                GetChildNodeInnerTextAsInt(node, "author-id"), _
                GetChildNodeInnerTextAsString(node, "author-name"), _
                GetChildNodeInnerTextAsInt(node, "category-id"), _
                GetChildNodeInnerTextAsString(node, "category-name"), _
                GetChildNodeInnerTextAsString(node, "body"), _
                GetChildNodeInnerTextAsString(node, "display-body"), _
                GetChildNodeInnerTextAsString(node, "display-extended-body"), _
                GetChildNodeInnerTextAsString(node, "extended-body"), _
                GetChildNodeInnerTextAsInt(node, "milestone-id"), _
                GetChildNodeInnerTextAsInt(node, "attachments-count"), _
                GetChildNodeInnerTextAsInt(node, "comments-count"), _
                GetChildNodeInnerTextAsInt(node, "from-client"), _
                GetChildNodeInnerTextAsDateTime(node, "posted-on"), _
                GetChildNodeInnerTextAsBoolean(node, "private"), _
                GetChildNodeInnerTextAsInt(node, "read-by-person"), _
                GetChildNodeInnerTextAsInt(node, "read-by-person-id"), _
                GetChildNodeInnerTextAsBoolean(node, "use-textile"))
        End Function

#End Region


#Region "Comments"

        ''' <summary>
        ''' Returns CommentDetails from Basecamp API
        ''' </summary>
        Public Function GetComment(ByVal comment_id As Integer) As CommentDetails
            Return GetCommentDetailsFromXmlNode(GetXmlDocumentResponse(String.Format("comments/{0}.xml", comment_id.ToString)).DocumentElement)
        End Function

        ''' <summary>
        ''' Returns a List(Of CommentDetails) from Basecamp API
        ''' </summary>
        Public Function GetComments(ByVal resource As String, ByVal resource_id As Integer) As List(Of CommentDetails)
            Return GetCommentDetailsCollectionFromXmlDocument(GetXmlDocumentResponse("{0}/{1}/comments.xml"))
        End Function

        ''' <summary>
        ''' Creates a comment in Basecamp via Basecamp API
        ''' </summary>
        Public Function CreateComment(ByVal resource As String, ByVal resource_id As Integer, ByVal body As String) As Integer
            Dim url As String = GetResponse(String.Format("{0}/{1}/comments.xml", resource, resource_id.ToString), "POST", GetCommentRequestXml(body))
            Return CType(url.Replace("/comments/", String.Empty).Replace(".xml", String.Empty), Integer)
        End Function

        ''' <summary>
        ''' Updates a comment in Basecamp via Basecamp API
        ''' </summary>
        Public Function UpdateComment(ByVal comment_id As Integer, ByVal body As String) As Boolean
            Dim resp As String = GetResponse(String.Format("comments/{0}.xml", comment_id.ToString), "PUT", GetCommentRequestXml(body))
            Return String.IsNullOrEmpty(resp.Trim)
        End Function

        ''' <summary>
        ''' Deletes a comment in Basecamp via Basecamp API
        ''' </summary>
        Public Function DeleteComment(ByVal comment_id As Integer) As Boolean
            Return String.IsNullOrEmpty(GetResponse(String.Format("comments/{0}.xml", comment_id.ToString), "DELETE").Trim)
        End Function

        ''' <summary>
        ''' Helper function to create Request Xml for comment requests
        ''' </summary>
        Private Shared Function GetCommentRequestXml(ByVal body As String) As String
            Return String.Format("<comment><body>{0}</body></comment>", HttpUtility.HtmlEncode(body))
        End Function

        ''' <summary>
        ''' Helper Function to create a List(Of PostDetails) from XmlDocument nodes
        ''' </summary>
        Private Shared Function GetCommentDetailsCollectionFromXmlDocument(ByVal doc As XmlDocument) As List(Of CommentDetails)
            Dim comments As New List(Of CommentDetails)
            Dim nodelist As XmlNodeList = doc.SelectNodes("//comment")
            Dim node As XmlNode = Nothing
            For Each node In nodelist
                comments.Add(GetCommentDetailsFromXmlNode(node))
            Next
            Return comments
        End Function

        ''' <summary>
        ''' Helper Function to create CommentDetails from XmlNode
        ''' </summary>
        Private Shared Function GetCommentDetailsFromXmlNode(ByVal node As XmlNode) As CommentDetails
            Return New CommentDetails( _
                GetChildNodeInnerTextAsInt(node, "id"), _
                GetChildNodeInnerTextAsInt(node, "commentable-id"), _
                GetChildNodeInnerTextAsString(node, "commentable-type"), _
                GetChildNodeInnerTextAsInt(node, "author-id"), _
                GetChildNodeInnerTextAsString(node, "body"), _
                GetChildNodeInnerTextAsInt(node, "attachments-count"), _
                GetChildNodeInnerTextAsString(node, "emailed-from"), _
                GetChildNodeInnerTextAsDateTime(node, "created-at"))
        End Function

#End Region

#Region "To-Do Lists"

        ''' <summary>
        ''' Returns ToDoListDetails from Basecamp API
        ''' </summary>
        Public Function GetToDoList(ByVal todo_list_id As Integer) As ToDoListDetails
            Return GetToDoListDetailsFromXmlNode(GetXmlDocumentResponse(String.Format("todo_lists/{0}.xml", todo_list_id.ToString)).DocumentElement)
        End Function

        ''' <summary>
        ''' Returns a List(Of ToDoListDetails) from Basecamp API
        ''' </summary>
        Public Overloads Function GetToDoLists(ByVal responsible_party As String) As List(Of ToDoListDetails)
            If Not String.IsNullOrEmpty(responsible_party) Then responsible_party = String.Format("?responsible_party={0}", responsible_party)
            Return GetToDoListDetailsCollectionFromXmlDocument(GetXmlDocumentResponse(String.Format("todo_lists.xml{0}", responsible_party)))
        End Function

        ''' <summary>
        ''' Returns a List(Of ToDoListDetails) from Basecamp API
        ''' </summary>
        Public Overloads Function GetToDoLists(ByVal project_id As Integer, ByVal filter As String) As List(Of ToDoListDetails)
            If Not String.IsNullOrEmpty(filter) Then filter = String.Format("?filter={0}", filter)
            Return GetToDoListDetailsCollectionFromXmlDocument(GetXmlDocumentResponse(String.Format("projects/{0}/todo_lists.xml{1}", project_id.ToString, filter)))
        End Function

        ''' <summary>
        ''' Creates a ToDoList in Basecamp via Basecamp API
        ''' </summary>
        Public Function CreateToDoList(ByVal project_id As Integer, ByVal milestone_id As Integer, ByVal name As String, ByVal description As String, ByVal tracked As Boolean, ByVal is_private As Boolean, ByVal todo_list_template_id As Integer) As Integer
            Dim url As String = GetResponse(String.Format("projects/{0}/todo_lists.xml", project_id.ToString), "POST", GetToDoListRequestXml(milestone_id, name, description, tracked, is_private, todo_list_template_id.ToString))
            Return CType(url.Replace(basecamp_url, String.Empty).Replace("todo_lists/", String.Empty).Replace(".xml", String.Empty), Integer)
        End Function

        ''' <summary>
        ''' Updates a ToDoList in Basecamp via Basecamp API
        ''' </summary>
        Public Function UpdateToDoList(ByVal todo_list_id As Integer, ByVal milestone_id As Integer, ByVal name As String, ByVal description As String, ByVal tracked As Boolean, ByVal is_private As Boolean) As Boolean
            Dim resp As String = GetResponse(String.Format("todo_lists/{0}.xml", todo_list_id.ToString), "PUT", GetToDoListRequestXml(milestone_id, name, description, tracked, is_private, String.Empty))
            Return String.IsNullOrEmpty(resp.Trim)
        End Function

        ''' <summary>
        ''' Deletes a ToDoList in Basecamp via Basecamp API
        ''' </summary>
        Public Function DeleteToDoList(ByVal todo_list_id As Integer) As Boolean
            Return String.IsNullOrEmpty(GetResponse(String.Format("todo_lists/{0}.xml", todo_list_id.ToString), "DELETE").Trim)
        End Function

        ''' <summary>
        ''' Reorders ToDoLists in Basecamp via Basecamp API
        ''' </summary>
        Public Function ReorderToDoLists(ByVal project_id As Integer, ByVal todo_list_ids As List(Of Integer)) As Boolean
            Dim resp As String = String.Empty
            If todo_list_ids.Count > 0 Then
                Dim todo_list_id As Integer = 0
                Dim sb As New StringBuilder
                sb.Append("<todo-lists type=""array"">")
                For Each todo_list_id In todo_list_ids
                    sb.Append(String.Format("<todo-list><id>{0}</id></todo-list>", todo_list_id.ToString))
                Next
                sb.Append("</todo-lists>")
                resp = GetResponse(String.Format("projects/{0}/todo_lists/reorder.xml", project_id.ToString), "POST", sb.ToString)
            End If
            Return String.IsNullOrEmpty(resp.Trim)
        End Function

        ''' <summary>
        ''' Helper function to create Request Xml for ToDoList requests
        ''' </summary>
        Private Shared Function GetToDoListRequestXml(ByVal milestone_id As Integer, ByVal name As String, ByVal description As String, _
                ByVal tracked As Boolean, ByVal is_private As Boolean, Optional ByVal todo_list_template_id As String = "") As String
            Dim sb As New StringBuilder
            sb.Append("<todo-list>")
            sb.Append(String.Format("<milestone-id>{0}</milestone-id>", milestone_id.ToString))
            sb.Append(String.Format("<name>{0}</name>", HttpUtility.HtmlEncode(name)))
            sb.Append(String.Format("<description>{0}</description>", HttpUtility.HtmlEncode(description)))
            sb.Append(String.Format("<tracked>{0}</tracked>", tracked.ToString))
            sb.Append(String.Format("<private>{0}</private>", is_private.ToString))
            If Not String.IsNullOrEmpty(todo_list_template_id) Then
                If Not String.Equals(todo_list_template_id, "0") Then
                    sb.Append("<todo-list-template-id>{0}</todo-list-template-id>", todo_list_template_id)
                Else
                    sb.Append("<todo-list-template-id />")
                End If
            End If
            sb.Append("</todo-list>")
            Return sb.ToString
        End Function

        ''' <summary>
        ''' Helper Function to create a List(Of ToDoListDetails) from XmlDocument nodes
        ''' </summary>
        Private Shared Function GetToDoListDetailsCollectionFromXmlDocument(ByVal doc As XmlDocument) As List(Of ToDoListDetails)
            Dim todolists As New List(Of ToDoListDetails)
            Dim nodelist As XmlNodeList = doc.SelectNodes("//todo-list")
            Dim node As XmlNode = Nothing
            For Each node In nodelist
                todolists.Add(GetToDoListDetailsFromXmlNode(node))
            Next
            Return todolists
        End Function

        ''' <summary>
        ''' Helper Function to create ToDoListDetails from XmlNode
        ''' </summary>
        Private Shared Function GetToDoListDetailsFromXmlNode(ByVal node As XmlNode) As ToDoListDetails

            Return New ToDoListDetails( _
                GetChildNodeInnerTextAsInt(node, "id"), _
                GetChildNodeInnerTextAsInt(node, "project-id"), _
                GetChildNodeInnerTextAsInt(node, "milestone-id"), _
                GetChildNodeInnerTextAsString(node, "name"), _
                GetChildNodeInnerTextAsString(node, "description"), _
                GetChildNodeInnerTextAsInt(node, "position"), _
                GetChildNodeInnerTextAsInt(node, "completed-count"), _
                GetChildNodeInnerTextAsInt(node, "uncompleted-count"), _
                GetChildNodeInnerTextAsBoolean(node, "tracked"), _
                GetChildNodeInnerTextAsBoolean(node, "private"), _
                GetChildNodeInnerTextAsBoolean(node, "complete"))

        End Function

#End Region

#Region "ToDoItems"

        ''' <summary>
        ''' Returns ToDoItemDetails from Basecamp API
        ''' </summary>
        Public Function GetToDoItem(ByVal todo_item_id As Integer) As ToDoItemDetails
            Return GetToDoItemDetailsFromXmlNode(GetXmlDocumentResponse(String.Format("todo_items/{0}.xml", todo_item_id.ToString)).DocumentElement)
        End Function

        ''' <summary>
        ''' Returns a List(Of ToDoItemDetails) from Basecamp API
        ''' </summary>
        Public Function GetToDoItems(ByVal todo_list_id As Integer) As List(Of ToDoItemDetails)
            Return GetToDoItemDetailsCollectionFromXmlDocument(GetXmlDocumentResponse(String.Format("todo_lists/{0}/todo_items.xml", todo_list_id.ToString)))
        End Function

        ''' <summary>
        ''' Sets a ToDoItem to complete in Basecamp via Basecamp API
        ''' </summary>
        Public Function CompleteToDoItem(ByVal todo_item_id As Integer) As Boolean
            Return String.IsNullOrEmpty(GetResponse(String.Format("todo_items/{0}/complete.xml", todo_item_id.ToString), "PUT").Trim)
        End Function

        ''' <summary>
        ''' Sets a ToDoItem to incomplete in Basecamp via Basecamp API
        ''' </summary>
        Public Function UncompleteToDoItem(ByVal todo_item_id As Integer) As Boolean
            Return String.IsNullOrEmpty(GetResponse(String.Format("todo_items/{0}/uncomplete.xml", todo_item_id.ToString), "PUT").Trim)
        End Function

        ''' <summary>
        ''' Creates a ToDoItem in Basecamp via Basecamp API
        ''' </summary>
        Public Function CreateToDoItem(ByVal todo_list_id As Integer, ByVal content As String, ByVal due_at As DateTime, ByVal notify As Boolean, ByVal responible_party As String) As Integer
            Dim url As String = GetResponse(String.Format("todo_lists/{0}/todo_items.xml", todo_list_id.ToString), "POST", GetToDoItemRequestXml(content, due_at, notify, responible_party))
            Return CType(url.Replace(basecamp_url, String.Empty).Replace("todo_items/", String.Empty), Integer)
        End Function

        ''' <summary>
        ''' Updates a ToDoItem in Basecamp via Basecamp API
        ''' </summary>
        Public Function UpdateToDoItem(ByVal todo_item_id As Integer, ByVal content As String, ByVal due_at As DateTime, ByVal notify As Boolean, ByVal responible_party As String) As Boolean
            Dim resp As String = GetResponse(String.Format("todo_items/{0}.xml", todo_item_id.ToString), "PUT", GetToDoItemRequestXml(content, due_at, notify, responible_party))
            Return String.IsNullOrEmpty(resp.Trim)
        End Function

        ''' <summary>
        ''' Deletes a ToDoItem in Basecamp via Basecamp API
        ''' </summary>
        Public Function DeleteToDoItem(ByVal todo_item_id As Integer) As Boolean
            Return String.IsNullOrEmpty(GetResponse(String.Format("todo_items/{0}.xml", todo_item_id.ToString), "DELETE").Trim)
        End Function


        ''' <summary>
        ''' Reorders ToDoItems in Basecamp via Basecamp API
        ''' </summary>
        Public Function ReorderToDoItems(ByVal todo_list_id As Integer, ByVal todo_item_ids As List(Of Integer)) As Boolean
            Dim resp As String = String.Empty
            If todo_item_ids.Count > 0 Then
                Dim todo_item_id As Integer = 0
                Dim sb As New StringBuilder
                sb.Append("<todo-items type=""array"">")
                For Each todo_item_id In todo_item_ids
                    sb.Append(String.Format("<todo-item><id>{0}</id></todo-item>", todo_item_id.ToString))
                Next
                sb.Append("</todo-items>")
                resp = GetResponse(String.Format("todo_lists/{0}/todo_items/reorder.xml", todo_list_id.ToString), "POST", sb.ToString)
            End If
            Return String.IsNullOrEmpty(resp.Trim)
        End Function

        ''' <summary>
        ''' Helper function to create Request Xml for ToDoItems requests
        ''' </summary>
        Private Shared Function GetToDoItemRequestXml(ByVal content As String, ByVal due_at As DateTime, ByVal notify As Boolean, ByVal responsible_party As String) As String
            Dim sb As New StringBuilder
            sb.Append("<todo-item>")
            sb.Append(String.Format("<content>{0}</content>", HttpUtility.HtmlEncode(content)))
            sb.Append(String.Format("<due-at>{0}</due-at>", due_at.ToString("u")))
            sb.Append(String.Format("<notify>{0}</notify>", notify.ToString))
            sb.Append(String.Format("<responsible_party>{0}</responsible_party>", responsible_party))
            sb.Append("</todo-item>")
            Return sb.ToString
        End Function


        ''' <summary>
        ''' Helper Function to create a List(Of ToDoItemDetails) from XmlDocument nodes
        ''' </summary>
        Private Shared Function GetToDoItemDetailsCollectionFromXmlDocument(ByVal doc As XmlDocument) As List(Of ToDoItemDetails)
            Dim todos As New List(Of ToDoItemDetails)
            Dim nodelist As XmlNodeList = doc.SelectNodes("//todo-item")
            Dim node As XmlNode = Nothing
            For Each node In nodelist
                todos.Add(GetToDoItemDetailsFromXmlNode(node))
            Next
            Return todos
        End Function

        ''' <summary>
        ''' Helper Function to create ToDoItemDetails from XmlNode
        ''' </summary>
        Private Shared Function GetToDoItemDetailsFromXmlNode(ByVal node As XmlNode) As ToDoItemDetails
            Return New ToDoItemDetails( _
                GetChildNodeInnerTextAsInt(node, "id"), _
                GetChildNodeInnerTextAsInt(node, "todo-list-id"), _
                GetChildNodeInnerTextAsInt(node, "creator-id"), _
                GetChildNodeInnerTextAsString(node, "content"), _
                GetChildNodeInnerTextAsInt(node, "comments-count"), _
                GetChildNodeInnerTextAsInt(node, "position"), _
                GetChildNodeInnerTextAsInt(node, "responsible-party-id"), _
                GetChildNodeInnerTextAsString(node, "responsible-party-type"), _
                GetChildNodeInnerTextAsDateTime(node, "due-at"), _
                GetChildNodeInnerTextAsBoolean(node, "completed"), _
                GetChildNodeInnerTextAsDateTime(node, "created-at"), _
                GetChildNodeInnerTextAsDateTime(node, "created-on"))
        End Function

#End Region

#Region "Milestones"

        ''' <summary>
        ''' Returns List(Of MilestoneDetails) from Basecamp API
        ''' </summary>
        Public Function GetMilestones(ByVal project_id As Integer, ByVal filter As String) As List(Of MilestoneDetails)
            Dim requestXml As String = String.Format("<request><find>{0}</find></request>", filter)
            If String.IsNullOrEmpty(filter) Then requestXml = String.Empty
            Return GetMilestoneDetailsCollectionFromXmlDocument(GetXmlDocumentResponse(String.Format("projects/{0}/milestones/list", project_id.ToString), requestXml:=requestXml))
        End Function

        ''' <summary>
        ''' Sets a milestone to complete in Basecamp via Basecamp API
        ''' </summary>
        Public Function CompleteMilestone(ByVal milestone_id As Integer) As MilestoneDetails
            Return GetMilestoneDetailsFromXmlNode(GetXmlDocumentResponse(String.Format("milestones/complete/{0}", milestone_id.ToString), "PUT").DocumentElement)
        End Function

        ''' <summary>
        ''' Sets a milestone to incomplete in Basecamp via Basecamp API
        ''' </summary>
        Public Function UncompleteMilestone(ByVal milestone_id As Integer) As MilestoneDetails
            Return GetMilestoneDetailsFromXmlNode(GetXmlDocumentResponse(String.Format("milestones/uncomplete/{0}", milestone_id.ToString), "PUT").DocumentElement)
        End Function

        ''' <summary>
        ''' Creates a milestone in Basecamp via Basecamp API
        ''' </summary>
        Public Function CreateMilestone(ByVal project_id As Integer, ByVal title As String, ByVal deadline As DateTime, ByVal responsible_party As String, ByVal notify As Boolean) As MilestoneDetails
            Dim requestXml As String = String.Format("<request>{0}</request>", GetMilestoneRequestXml(title, deadline, responsible_party, notify))
            Dim dl As List(Of MilestoneDetails) = GetMilestoneDetailsCollectionFromXmlDocument(GetXmlDocumentResponse(String.Format("projects/{0}/milestones/create", project_id.ToString), "POST", requestXml))
            If dl.Count > 0 Then
                Return dl(0)
            Else
                Return Nothing
            End If
        End Function

        ''' <summary>
        ''' Updates a milestone in Basecamp via Basecamp API
        ''' </summary>
        Public Overloads Function UpdateMilestone(ByVal milestone_id As Integer, ByVal title As String, ByVal deadline As DateTime, ByVal responsible_party As String, ByVal notify As Boolean) As MilestoneDetails
            Dim requestXml As String = String.Format("<request>{0}</request>", GetMilestoneRequestXml(title, deadline, responsible_party, notify))
            Return UpdateMilestoneHelper(milestone_id, requestXml)
        End Function

        ''' <summary>
        ''' Updates a milestone in Basecamp via Basecamp API
        ''' </summary>
        Public Overloads Function UpdateMilestone(ByVal milestone_id As Integer, ByVal title As String, ByVal deadline As DateTime, ByVal responsible_party As String, ByVal notify As Boolean, ByVal move_upcoming_milestones As Boolean, ByVal move_upcoming_milesones_off_weekends As Boolean) As MilestoneDetails
            Dim requestXml As String = String.Format("<request>{0}<move-upcoming-milestones>{1}</move-upcoming-milestones><move-upcoming-milestones-off-weekends>{2}</move-upcoming-milestones-off-weekends></request>", GetMilestoneRequestXml(title, deadline, responsible_party, notify), move_upcoming_milestones.ToString, move_upcoming_milesones_off_weekends.ToString)
            Return UpdateMilestoneHelper(milestone_id, requestXml)
        End Function

        ''' <summary>
        ''' Helper function for updating milestone
        ''' </summary>
        ''' <remarks>Milestone API specification differs from any of the others</remarks>
        Private Function UpdateMilestoneHelper(ByVal milestone_id As Integer, ByVal requestXml As String) As MilestoneDetails
            Try
                Return GetMilestoneDetailsFromXmlNode(GetXmlDocumentResponse(String.Format("/milestones/update/{0}", milestone_id.ToString), "POST", requestXml).DocumentElement)
            Catch ex As Exception
                Return Nothing
            End Try
        End Function

        ''' <summary>
        ''' Deletes a milestone in Basecamp via Basecamp API
        ''' </summary>
        Public Function DeleteMilestone(ByVal milestone_id As Integer) As MilestoneDetails
            Try
                Return GetMilestoneDetailsFromXmlNode(GetXmlDocumentResponse(String.Format("milestones/delete/{0}", milestone_id.ToString), "POST").DocumentElement)
            Catch ex As Exception
                Return Nothing
            End Try
        End Function

        ''' <summary>
        ''' Helper function to create Request Xml for Milestone requests
        ''' </summary>
        Private Shared Function GetMilestoneRequestXml(ByVal title As String, ByVal deadline As DateTime, ByVal responsible_party As String, ByVal notify As Boolean) As String
            Dim sb As New StringBuilder
            sb.Append("<milestone>")
            sb.Append(String.Format("<title>{0}</title>", HttpUtility.HtmlEncode(title)))
            sb.Append(String.Format("<deadline>{0}</deadline>", deadline.ToString("u")))
            sb.Append(String.Format("<responsible-party>{0}</responsible-party>", responsible_party))
            sb.Append(String.Format("<notify>{0}</notify>", notify.ToString))
            sb.Append("</milestone>")
            Return sb.ToString
        End Function

        ''' <summary>
        ''' Helper Function to create a List(Of MilestoneDetails) from XmlDocument nodes
        ''' </summary>
        Private Shared Function GetMilestoneDetailsCollectionFromXmlDocument(ByVal doc As XmlDocument) As List(Of MilestoneDetails)
            Dim milestones As New List(Of MilestoneDetails)
            Dim nodelist As XmlNodeList = doc.SelectNodes("//milestone")
            Dim node As XmlNode = Nothing
            For Each node In nodelist
                milestones.Add(GetMilestoneDetailsFromXmlNode(node))
            Next
            Return milestones
        End Function

        ''' <summary>
        ''' Helper Function to create MilestoneDetails from XmlNode
        ''' </summary>
        Private Shared Function GetMilestoneDetailsFromXmlNode(ByVal node As XmlNode) As MilestoneDetails
            Return New MilestoneDetails( _
                GetChildNodeInnerTextAsInt(node, "id"), _
                GetChildNodeInnerTextAsInt(node, "project-id"), _
                GetChildNodeInnerTextAsString(node, "title"), _
                GetChildNodeInnerTextAsDateTime(node, "deadline"), _
                GetChildNodeInnerTextAsInt(node, "comments-count"), _
                GetChildNodeInnerTextAsBoolean(node, "completed"), _
                GetChildNodeInnerTextAsDateTime(node, "completed-on"), _
                GetChildNodeInnerTextAsInt(node, "completer-id"), _
                GetChildNodeInnerTextAsInt(node, "responsible-party-id"), _
                GetChildNodeInnerTextAsString(node, "responsible-party-type"), _
                GetChildNodeInnerTextAsBoolean(node, "wants-notification"), _
                GetChildNodeInnerTextAsDateTime(node, "created-on"), _
                GetChildNodeInnerTextAsInt(node, "creator-id"))
        End Function

#End Region

#Region "TimeEntries"


        ''' <summary>
        ''' Returns TimeEntryDetails from Basecamp API
        ''' </summary>
        Public Function GetTimeEntry(ByVal time_entry_id As Integer) As TimeEntryDetails
            Return GetTimeEntryDetailsFromXmlNode(GetXmlDocumentResponse(String.Format("time_entries/{0}.xml", time_entry_id.ToString)).DocumentElement)
        End Function

        ''' <summary>
        ''' Returns List(Of TimeEntryDetails) for a given project from Basecamp API
        ''' </summary>
        Public Function GetTimeEntriesByProject(ByVal project_id As Integer, ByVal pagenumber As Integer) As List(Of TimeEntryDetails)
            Dim requestString As String = String.Format("projects/{0}/time_entries.xml", project_id.ToString)
            If pagenumber > 0 Then requestString = String.Format("{0}?page={1}", requestString, pagenumber.ToString)
            Return GetTimeEntryDetailsCollectionFromXmlDocument(GetXmlDocumentResponse(requestString))
        End Function

        ''' <summary>
        ''' Returns List(Of TimeEntryDetails) for a given ToDoItem from Basecamp API
        ''' </summary>
        Public Function GetTimeEntriesByToDoItem(ByVal todo_item_id As Integer) As List(Of TimeEntryDetails)
            Return GetTimeEntryDetailsCollectionFromXmlDocument(GetXmlDocumentResponse(String.Format("todo_items/{0}/time_entries.xml", todo_item_id.ToString)))
        End Function

        ''' <summary>
        ''' Returns List(Of TimeEntryDetails) by report filters from Basecamp API
        ''' </summary>
        Public Function GetTimeEntriesReport(ByVal from_date As DateTime, ByVal to_date As DateTime, ByVal person_id As Integer, ByVal todo_item_id As Integer, ByVal project_id As Integer, ByVal company_id As Integer) As List(Of TimeEntryDetails)

            Dim requestXml As String = String.Empty

            If Math.Abs(DateDiff(DateInterval.Month, from_date, to_date)) > 6 Then
                to_date = from_date.AddMonths(-6)
            End If

            requestXml = String.Format("time_entries/report.xml?from={0}&to={1}", ConvertDateTimeToQueryStringFormat(from_date), ConvertDateTimeToQueryStringFormat(to_date))

            If person_id > 0 Then
                If requestXml.Length > 0 Then requestXml &= "&"
                requestXml &= String.Format("subject_id={0}", person_id.ToString)
            End If
            If todo_item_id > 0 Then
                If requestXml.Length > 0 Then requestXml &= "&"
                requestXml &= String.Format("todo_item_id={0}", todo_item_id.ToString)
            End If
            If project_id > 0 Then
                If requestXml.Length > 0 Then requestXml &= "&"
                requestXml &= String.Format("filter_project_id={0}", project_id.ToString)
            End If
            If company_id > 0 Then
                If requestXml.Length > 0 Then requestXml &= "&"
                requestXml &= String.Format("filter_company_id={0}", company_id.ToString)
            End If
            Return GetTimeEntryDetailsCollectionFromXmlDocument(GetXmlDocumentResponse(requestXml))
        End Function

        ''' <summary>
        ''' Creates a TimeEntry for a given project in Basecamp via Basecamp API
        ''' </summary>
        Public Function CreateProjectTimeEntry(ByVal project_id As Integer, ByVal person_id As Integer, ByVal entry_date As DateTime, ByVal hours As Double, ByVal description As String) As Integer
            Dim url As String = GetResponse(String.Format("projects/{0}/time_entries.xml", project_id.ToString), "POST", GetTimeEntryRequestXml(person_id, entry_date, hours, description, 0))
            Return CType(url.Replace(basecamp_url, String.Empty).Replace("time_entries/", String.Empty).Replace(".xml", String.Empty), Integer)
        End Function


        ''' <summary>
        ''' Creates a TimeEntry for a given ToDoItem in Basecamp via Basecamp API
        ''' </summary>
        Public Function CreateToDoItemTimeEntry(ByVal todo_item_id As Integer, ByVal person_id As Integer, ByVal entry_date As DateTime, ByVal hours As Double, ByVal description As String) As Integer
            Dim url As String = GetResponse(String.Format("todo_items/{0}/time_entries.xml", todo_item_id.ToString), "POST", GetTimeEntryRequestXml(person_id, entry_date, hours, description, 0))
            Return CType(url.Replace(basecamp_url, String.Empty).Replace("time_entries/", String.Empty).Replace(".xml", String.Empty), Integer)
        End Function

        ''' <summary>
        ''' Updates a TimeEntry in Basecamp via Basecamp API
        ''' </summary>
        Public Function UpdateTimeEntry(ByVal time_entry_id As Integer, ByVal person_id As Integer, ByVal entry_date As DateTime, ByVal hours As Double, ByVal description As String, ByVal todo_item_id As Integer) As Boolean
            Dim resp As String = GetResponse(String.Format("time_entries/{0}.xml", time_entry_id.ToString), "PUT", GetTimeEntryRequestXml(person_id, entry_date, hours, description, todo_item_id))
            Return String.IsNullOrEmpty(resp.Trim)
        End Function

        ''' <summary>
        ''' Deletes a TimeEntry in Basecamp via Basecamp API
        ''' </summary>
        Public Function DeleteTimeEntry(ByVal time_entry_id As Integer) As Boolean
            Return String.IsNullOrEmpty(GetResponse(String.Format("time_entries/{0}.xml", time_entry_id.ToString), "DELETE").Trim)
        End Function

        ''' <summary>
        ''' Helper function to create Request Xml for TimeEntry requests
        ''' </summary>
        Private Shared Function GetTimeEntryRequestXml(ByVal person_id As Integer, ByVal entry_date As DateTime, ByVal hours As Double, ByVal description As String, ByVal todo_item_id As Integer) As String
            Dim sb As New StringBuilder
            sb.Append("<time-entry>")
            sb.Append(String.Format("<person-id>{0}</person-id>", person_id.ToString))
            sb.Append(String.Format("<date>{0}</date>", entry_date.ToString("u")))
            sb.Append(String.Format("<hours>{0}</hours>", hours.ToString))
            sb.Append(String.Format("<description>{0}</description>", description))
            If todo_item_id > 0 Then sb.Append(String.Format("<todo-item-id>{0}</todo-item-id>", todo_item_id.ToString))
            sb.Append("</time-entry>")
            Return sb.ToString
        End Function

        ''' <summary>
        ''' Helper Function to create a List(Of TimeEntryDetails) from XmlDocument nodes
        ''' </summary>
        Private Shared Function GetTimeEntryDetailsCollectionFromXmlDocument(ByVal doc As XmlDocument) As List(Of TimeEntryDetails)
            Dim entries As New List(Of TimeEntryDetails)
            Dim nodelist As XmlNodeList = doc.SelectNodes("//time-entry")
            Dim node As XmlNode = Nothing
            For Each node In nodelist
                entries.Add(GetTimeEntryDetailsFromXmlNode(node))
            Next
            Return entries
        End Function

        ''' <summary>
        ''' Helper Function to create TimeEntryDetails from XmlNode
        ''' </summary>
        Private Shared Function GetTimeEntryDetailsFromXmlNode(ByVal node As XmlNode) As TimeEntryDetails
            Return New TimeEntryDetails( _
                GetChildNodeInnerTextAsInt(node, "id"), _
                GetChildNodeInnerTextAsInt(node, "person-id"), _
                GetChildNodeInnerTextAsInt(node, "todo-item-id"), _
                GetChildNodeInnerTextAsInt(node, "project-id"), _
                GetChildNodeInnerTextAsDateTime(node, "date"), _
                GetChildNodeInnerTextAsString(node, "description"), _
                GetChildNodeInnerTextAsDouble(node, "hours"))
        End Function

#End Region


        ''' <summary>
        ''' Helper function to create an instance of BasecampRestAPIDataProvider
        ''' </summary>
        Private Shared _Instance As BasecampRestAPIDataProvider = Nothing
        Public Shared Function Instance() As BasecampRestAPIDataProvider
            If _Instance Is Nothing Then
                _Instance = New BasecampRestAPIDataProvider
            End If
            Return _Instance
        End Function

    End Class

End Namespace